package labyrinth;

import gui.threed.LabyrinthModel;

import java.util.Random;

import javax.vecmath.Vector2d;

import Jama.Matrix;

/**
 * A Class which represents a Labyrinth. It also holds a 3D representation.
 * Maybe we should divide that into another class.
 * 
 * @author Johannes Kulick
 * @version 0.1
 * @licence GNU Lesser General Public License
 */
public class Labyrinth {

	/**
	 * The intern modelling of a labyrinth is a nxm-Matrix with integer values
	 * describing the state of a block (states see below)
	 */
	private int[][] mLabyrinth;

	/**
	 * The 3D view of the labyrinth. We save it for performance reasons
	 */
	private LabyrinthModel m3DView;

	/***************************************************************************
	 * Block states
	 ***************************************************************************/
	/**
	 * FREE is a normal way, where an agent can move on
	 */
	public final static int FREE = 0;

	/**
	 * WALL is a wall, which can not be passed
	 */
	public final static int WALL = 1;

	/**
	 * Startpoint for an agent
	 */
	public final static int START = 2;

	/**
	 * Goalpoint
	 */
	public final static int GOAL = 3;

	/**
	 * The field where the agent stands
	 */
	public final static int AGENT = 4;

	/**
	 * An unknown field
	 */
	public final static int UNKNOWN = 5;

	/**
	 * Standard C'tor, which sets the labyrinth-block states
	 * 
	 * @param labyrinth
	 *            The block state matrix. Use Block states as described above.
	 */
	public Labyrinth(int[][] labyrinth) {
		mLabyrinth = labyrinth;
		m3DView = new LabyrinthModel(this);
	}

	/**
	 * Gives a 3D View of the Labyrinth
	 * 
	 * @return A Branch Group which represents the Labyrinth
	 */
	public LabyrinthModel get3dView() {
		return m3DView;
	}

	/**
	 * Returns the labyrinth in matrixform
	 * 
	 * @return the matrixform of the labyrinth
	 */
	public int[][] getMatrix() {
		return mLabyrinth;
	}

	/**
	 * Creates a labyrinth in matrix format on a given Integer Matrix
	 * 
	 * @param matrix
	 *            the underlying matrix, which determines the size of the
	 *            labyrinth. With this option you can do several passes with
	 *            different option, such as different granualrity
	 * @param minWallLength
	 *            minimum length of a wall
	 * @param maxWallLength
	 *            maximum length of a wall
	 * @param granularity
	 *            Width of ways
	 * @param numWalls
	 *            Number of walls that will be created
	 * @return
	 */
	public static int[][] createLabyrinth(int[][] matrix, int minWallLength,
			int maxWallLength, int granularity, int numWalls) {

		// create a wall around the labyrinth
		for (int x = 0; x < matrix.length; x++) {
			for (int y = 0; y < matrix[x].length; y++) {
				if (x == 0 || x == matrix.length - 1 || y == 0
						|| y == matrix[x].length - 1)
					matrix[x][y] = WALL;
			}
		}

		int xsize = matrix.length;
		int ysize = matrix[0].length;

		// Startpoint and Goal
		matrix[(int) (xsize / 2 - 0.5)][0] = GOAL;
		matrix[(int) (xsize / 2 - 0.5)][ysize - 1] = START;

		int x, y, direction, length;
		Random r = new Random();

		for (int i = 0; i < numWalls; i++) {
			// get startpoint
			x = granularity * r.nextInt((xsize - 1) / granularity) + 2;
			y = granularity * r.nextInt((ysize - 1) / granularity) + 2;

			// don't create overlapping walls
			if (matrix[x][y] == WALL) {
				continue;
			}

			// random direction of the following integer format:
			// 0 : up
			// 1 : right
			// 2 : down
			// 3 : left
			direction = r.nextInt(4);

			// length of wall
			length = granularity
					* (minWallLength + r.nextInt(maxWallLength - minWallLength))
					+ 1;
			// create wall
			for (int j = 0; j < length; j++) {
				// avoid out of bounce exception and non-accessible fields
				if (x < 0 || x >= matrix.length || y < 0
						|| y >= matrix[0].length || matrix[x][y] == WALL)
					break;

				// create wall on <x, y>
				matrix[x][y] = WALL;

				// move into the given direction with modulo-magic
				x = x + (direction % 2) * (int) Math.signum(direction - 2);
				y = y + (direction % 2 - 1) * (int) Math.signum(direction - 1);
			}

		}
		return matrix;
	}

	/**
	 * Returns the startpoint of the labyrinth
	 * 
	 * @return A int-array representing the startpoint ([0] -> x-coord, [1] ->
	 *         y-coord)
	 */
	public Position getStartPoint() {
		Position startpoint = new Position();
		for (int x = 0; x < mLabyrinth.length; x++) {
			for (int y = 0; y < mLabyrinth[x].length; y++) {
				if (mLabyrinth[x][y] == START) {
					startpoint.x = x;
					startpoint.y = y;
				}
			}
		}
		return startpoint;
	}

	/**
	 * Computes whether two lines given by start- and endpoint intersect.
	 * 
	 * @param startOne
	 *            Startpoint for line one
	 * @param endOne
	 *            Endpoint for line one
	 * @param startTwo
	 *            Startpoint for line two
	 * @param endTwo
	 *            Endpoint for line two
	 * @return true if the lines intersect, false otherwise
	 */
	private static boolean intersectLines(Vector2d startOne, Vector2d endOne,
			Vector2d startTwo, Vector2d endTwo) {
		Vector2d mOne = new Vector2d();
		mOne.sub(endOne, startOne);

		Vector2d mTwo = new Vector2d();
		mTwo.sub(endTwo, startTwo);

		Matrix lin = new Matrix(2, 2);
		lin.set(0, 0, mOne.getX());
		lin.set(1, 0, mOne.getY());
		lin.set(0, 1, -mTwo.getX());
		lin.set(1, 1, -mTwo.getY());

		Matrix result = new Matrix(2, 1);
		result.set(0, 0, startTwo.getX() - startOne.getX());
		result.set(1, 0, startTwo.getY() - startOne.getY());

		Matrix x = lin.inverse().times(result);

		double xOne = x.get(0, 0);
		double xTwo = x.get(1, 0);

		if (xOne < 0.0 || xOne >= 1.0 || xTwo < 0.0 || xTwo > 1.0)
			return false;
		else
			return true;
	}

	/**
	 * Return true if between the agent and the given position is a line of
	 * sight.
	 * 
	 * @param agent Position of the agent
	 * @param labPosition Position in the labyrinth
	 * @return
	 */
	public boolean isLineOfSight(Position agent, Position labPosition) {
		Vector2d start = new Vector2d(agent.x + 0.5, agent.y + 0.5);
		Vector2d end = new Vector2d(labPosition.x, labPosition.y);

		for (int x = Math.min(agent.x, labPosition.x); x <= Math.max(agent.x,
				labPosition.x); x++) {
			for (int y = Math.min(agent.y, labPosition.y); y <= Math.max(
					agent.y, labPosition.y); y++) {
				if (getMatrix()[x][y] == Labyrinth.WALL) {
					// if one side of a block intersects with our eyeline it's
					// not a line of sight
					if (Labyrinth.intersectLines(start, end,
							new Vector2d(x, y), new Vector2d(x, y + 1))
							|| Labyrinth.intersectLines(start, end,
									new Vector2d(x, y + 1), new Vector2d(x + 1,
											y + 1))
							|| Labyrinth.intersectLines(start, end,
									new Vector2d(x, y), new Vector2d(x + 1, y))
							|| Labyrinth.intersectLines(start, end,
									new Vector2d(x + 1, y), new Vector2d(x + 1,
											y + 1))) {
						return false;
					}
				}
			}
		}
		return true;
	}

}
